﻿/*
 * Copyright 2011 Edward L. Stauff.                           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "XerialContext.cs", is part of the "Stauffware.Common.Xml.Xerialization" 
 * library.  "Stauffware.Common.Xml.Xerialization" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Xml.Xerialization" is distributed in the hope that it will be 
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common.Xml.Xerialization".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using System.IO;
using Stauffware.Common.Core;

namespace Stauffware.Common.Xml.Xerialization
{
	/// <summary>
	/// The top-level xerialization object.  Its main purpose is to keep track of
	/// type mappings and provide access to them.
	/// </summary>
	/// <remarks>
	/// A XerialContext is a heavyweight object that can bear a significant cost
	/// to create and setup, depending on the number of types to be handled.
	/// You should create it once and keep it around if you're going to use it more than once.
	/// </remarks>

	public class XerialContext
	{
		/// <summary>
		/// Gets whether the case of XML element names and attribute names will be ignored.
		/// </summary>
		public readonly bool IgnoreXmlCase;

		/// <summary>
		/// Contains all the type mappings for this context.  This will never be null.
		/// </summary>
		public readonly TypeMappingDictionary TypeMappings;

		/// <summary>
		/// The field encoding to use when it's not specified at the class or member level.
		/// </summary>
		public MemberValueEncoding DefaultFieldEncoding = MemberValueEncoding.Automatic;

		/// <summary>
		/// The property encoding to use when it's not specified at the class or member level.
		/// </summary>
		public MemberValueEncoding DefaultPropertyEncoding = MemberValueEncoding.Automatic;

		/// <summary>
		/// The field inclusion to use when it's not specified at the class level.
		/// </summary>
		public MemberInclusion DefaultFieldInclusion = MemberInclusion.All;

		/// <summary>
		/// The field inclusion to use when it's not specified at the class level.
		/// </summary>
		public MemberInclusion DefaultPropertyInclusion = MemberInclusion.All;

		/// <summary>
		/// Specifies whether class members are optional, if not specified at the
		/// class or member level.
		/// </summary>
		public bool DefaultMemberOptionality = false;

		/// <summary>
		/// Specifies whether to ignore unknown XML elements, if this is not specified
		/// at the class level.
		/// </summary>
		public bool DefaultIgnoreUnknownElements = false;

		/// <summary>
		/// Specifies whether to ignore unknown XML elements, if this is not specified
		/// at the class level.
		/// </summary>
		public bool DefaultIgnoreUnknownAttributes = false;

		//-----------------------------------------------------------------------------
		/// <summary> Constructor. </summary>
		/// <param name="ignoreXmlCase">
		/// Whether the context will ignore the case of XML element and attribute names.
		/// </param>

		public XerialContext (bool ignoreXmlCase)
		{
			this.IgnoreXmlCase = ignoreXmlCase;
			this.TypeMappings = new TypeMappingDictionary(this);
			this.TypeMappings.AddStandardMappings();
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns an encoding for the given type, determined automatically according
		/// to global hard-coded rules.  This will never return Default or Automatic.
		/// </summary>

		internal MemberValueEncoding GetAutomaticEncoding (Type t)
		{
			XerialTypeMapping tp = this.TypeMappings[t];
			if (tp != null)
			{
				if (tp.DefaultXmlType == XmlType.Attribute) return MemberValueEncoding.Attribute;
				if (tp.DefaultXmlType == XmlType.Element) return MemberValueEncoding.NestedElements;
				if (tp.DefaultXmlType == XmlType.Text) return MemberValueEncoding.Text;
				Assert.NotReached();
			}
			if (t.IsValueType)
				return MemberValueEncoding.Attribute;
			if (t.IsClass)
				return MemberValueEncoding.NestedElements;

			Assert.NotReached();
			return MemberValueEncoding.Omit;	// shut compiler up
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns whether two XML element or attribute names match, based on whether
		/// we're ignoring case.
		/// </summary>

		internal bool XmlNamesMatch (string name1, string name2)
		{
			if (this.IgnoreXmlCase)
				return name1.ToLower() == name2.ToLower();
			else return name1 == name2;
		}

		//-----------------------------------------------------------------------------

		#region Writing to XML

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes an object to the given XML file.  
		/// If the file already exists, it is replaced.
		/// </summary>
		/// <param name="targetObj">the object to be written</param>
		/// <param name="filePath">the file that will receive the XML</param>

		public void WriteToXml (object targetObj, string filePath)
		{
			StreamWriter sw = new StreamWriter(filePath);
			try
			{
				this.WriteToXml(targetObj, sw);
			}
			finally
			{
				sw.Close();
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes an object in XML form to the given TextWriter.
		/// </summary>
		/// <param name="targetObj">the object to be written</param>
		/// <param name="txtWrtr">where to write the XML</param>

		public void WriteToXml (object targetObj, TextWriter txtWrtr)
		{
			XmlTextWriter xtw = new XmlTextWriter(txtWrtr);
			xtw.Formatting = Formatting.Indented;
			xtw.Indentation = 1;
			xtw.IndentChar = '\t';
			this.WriteToXml(targetObj, xtw);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes an object to the given XmlTextWriter.
		/// </summary>
		/// <param name="targetObj">the object to be written</param>
		/// <param name="writer">where to write the XML</param>

		public void WriteToXml (object targetObj, XmlWriter writer)
		{
			if (targetObj == null)
				return;

			XerialTypeMapping mapping = this.TypeMappings[targetObj.GetType()];
			Require.NotNull(mapping, "XerialContext has no mapping for type " + targetObj.GetType().FullName);

			mapping.WriteToXmlElmnt(null, targetObj, writer);
		}

		//-----------------------------------------------------------------------------

		#endregion

		#region Reading from XML

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from an XML file.
		/// </summary>
		/// <param name="filePath">the name or path of the XML file to read</param>
		/// <param name="expectedRootElement">the name of the root (document) level element</param>
		/// <param name="errorReporter">
		///		<para><c>bool errorReporter(XmlError)</c></para>
		///		<para>If provided, this delegate will be called for each potentially 
		///		non-fatal XML error.</para>  
		///		<para>Its return value tells whether to continue processing the XML.</para>
		/// </param>
		/// <returns>the object created from the contents of the XML file</returns>

		public object ReadFromXml (string filePath, string expectedRootElement = null, XmlErrorReporter errorReporter = null)
		{
			StreamReader sr = new StreamReader(filePath);
			try
			{
				return this.ReadFromXml(sr, expectedRootElement, errorReporter);
			}
			finally
			{
				sr.Close();
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from a TextReader.
		/// </summary>
		/// <param name="tr">where to read the XML input from</param>
		/// <param name="expectedRootElement">the name of the root (document) level element</param>
		/// <param name="errorReporter">
		///		<para><c>bool errorReporter(XmlError)</c></para>
		///		<para>If provided, this delegate will be called for each potentially 
		///		non-fatal XML error.</para>  
		///		<para>Its return value tells whether to continue processing the XML.</para>
		/// </param>
		/// <returns>the object created from the XML</returns>

		public object ReadFromXml (TextReader tr, string expectedRootElement = null, XmlErrorReporter errorReporter = null)
		{
			return this.ReadFromXml(new NiceXmlReader(tr, errorReporter), expectedRootElement);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from a XmlReader.
		/// </summary>
		/// <param name="reader">where to read the XML input from</param>
		/// <param name="expectedRootElement">the name of the root (document) level element</param>
		/// <returns>the object created from the XML</returns>

		public object ReadFromXml (XmlReader reader, string expectedRootElement = null)
		{
			return this.ReadFromXml(new NiceXmlReader(reader), expectedRootElement);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from a NiceXmlReader.
		/// </summary>
		/// <param name="reader">where to read the XML input from</param>
		/// <param name="expectedRootElement">the name of the root (document) level element</param>
		/// <returns>the object created from the XML</returns>

		public object ReadFromXml (NiceXmlReader reader, string expectedRootElement = null)
		{
			reader.MoveToContent();

			// Note: if we don't find the right thing here, there's no point in continuing.

			if (reader.NodeType != XmlNodeType.Element)
				throw new XmlError(reader, "expecting an element; found " + reader.NodeType.ToString() + " instead");

			if (expectedRootElement != null && !this.XmlNamesMatch(expectedRootElement, reader.Name))
				throw new XmlError(reader, "expecting root element \"" + expectedRootElement + 
											"; found \"" + reader.Name + "\" instead");

			XerialTypeMapping mapping = this.TypeMappings[reader.Name];
			if (mapping == null)
				throw new XmlError(reader, "unknown XML element name: \"" + reader.Name + "\"");

			return mapping.ReadFromXmlElmnt(reader);
		}

		//-----------------------------------------------------------------------------

		#endregion

		#region debugging stuff

		/// <summary>
		/// Assign this to get debugging output.
		/// </summary>
		public System.IO.TextWriter DebugLog;

		/// <summary>
		/// Returns whether the debug log is enabled.
		/// </summary>
		protected bool DebugLogEnabled { get { return this.DebugLog != null; } }

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes a line of debugging output.
		/// </summary>

		protected void LogLine (string str)
		{
			DebugLog.WriteLine(str);
		}

		//-----------------------------------------------------------------------------

		#endregion

	}
}
